package jcrosswords.domain;

import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import jcrosswords.bo.ClueResponseBO;
import jcrosswords.bo.impl.ClueResponseBOImpl;

public class Board {
	
	private Cell[][] board;
	private List<Coordinates> availableRandomCoordinates;
	private Map<String, List<Orientation>> availableRandomOrientations;
	private Map<String, List<Direction>> availableRandomDirections;
	private Map<String, List<ClueResponse>> availableRandomClueResponses;
	
	private Set<BoardChangeListener> boardChangeListeners; 
	
	private Random random;
	private ClueResponseBO clueResponseBO;

	public Board(int width, int height) {
		board = new Cell[width][height];
		availableRandomCoordinates = new ArrayList<Coordinates>();
		availableRandomOrientations = new HashMap<String, List<Orientation>>();
		availableRandomDirections = new HashMap<String, List<Direction>>();
		availableRandomClueResponses = new HashMap<String, List<ClueResponse>>();
		for(int x = 0; x < board.length; x++) {
			for(int y = 0; y < board[0].length; y++) {
				Coordinates cc = new Coordinates(x, y);
				board[x][y] = new Cell(cc, null);
				availableRandomCoordinates.add(cc);
			}
		}
		boardChangeListeners = new HashSet<BoardChangeListener>();
		random = new Random();
		clueResponseBO = new ClueResponseBOImpl();
	}
		
	public int getWidth() {
		return board.length;
	}
		
	public int getHeight() {
		return board[0].length;
	}
		
	public Cell getCell(Coordinates coordinates) {
		return board[coordinates.getX()][coordinates.getY()];
	}
	
	public void addBoardChangeListener(BoardChangeListener listener) {
		boardChangeListeners.add(listener);
	}
	
	public void removeBoardChangeListener(BoardChangeListener listener) {
		boardChangeListeners.remove(listener);
	}
	
	private static int recursionLevel;
	
	public boolean populateBoard() {
		recursionLevel++;
		for(int i = 0; i < recursionLevel; i++) {
			System.out.print("*");
		}
		System.out.println();
		
		if(!isBoardFull()) {
			
			// random coordinates
			while(getAvailableRandomCoordinatesSize() > 0) {
				Coordinates cc = getRandomCoordinates();
				
				// random orientation
				availableRandomOrientations.put(cc.toString(), new ArrayList<Orientation>(Arrays.asList(Orientation.values())));
				while(getAvailableRandomOrientationsSize(cc) > 0) {
					Orientation o = getRandomOrientation(cc);
					
					// random direction
					availableRandomDirections.put(cc.toString() + o.toString(), new ArrayList<Direction>(Arrays.asList(Direction.values())));
					while(getAvailableRamdomDirectionsSize(cc, o) > 0) {
						Direction d = getRamdomDirection(cc, o);
						
						// is useful?
						if(isUseful(cc, o, d)) {
							
							// random clue response
							String hint = getHint(cc, o, d);
							availableRandomClueResponses.put(cc.toString() + o.toString() + d.toString(), clueResponseBO.getClueResponses(hint));
							while(getAvailableRamdomClueResponsesSize(cc, o, d) > 0) {
								ClueResponse cr = getRamdomClueResponse(cc, o, d);
								
								ClueResponseInstance clueResponseInstance = new ClueResponseInstance(cr, cc, o, d);
								
								// insert into board
								insertClueResponseInstance(clueResponseInstance);
								
								// recursion
								if (!populateBoard()) {
									
									// remove from board
									removeClueResponseInstance(clueResponseInstance);
									
								}
								else {
									return true;
								}
							
								availableRandomClueResponses.get(cc.toString() + o.toString() + d.toString()).remove(cr);
							}
						}
						
						availableRandomDirections.get(cc.toString() + o.toString()).remove(d);
					}
					
					availableRandomOrientations.get(cc.toString()).remove(o);
				}
				
//				availableRandomCoordinates.remove(cc);
				recursionLevel--;
				for(int i = 0; i < recursionLevel; i++) {
					System.out.print("*");
				}
				System.out.println();
				return false;
				
			}
			
		}
		
		return true;
	}
	
	private Boolean isUseful(Coordinates cc, Orientation o, Direction d) {
		if (cc == null || o == null || d == null) {
			throw new IllegalArgumentException();
		}
		if (cc.getX() == 0) {
			if(o == Orientation.WEST) {
				return false;
			}
			if(o == Orientation.NORTH || o == Orientation.SOUTH) {
				if(d == Direction.WEST) {
					return false;
				}
			}
		}
		if (cc.getX() == getWidth() - 1) {
			if(o == Orientation.EAST) {
				return false;
			}
			if(o == Orientation.NORTH || o == Orientation.SOUTH) {
				if(d == Direction.EAST) {
					return false;
				}
			}
		}
		if (cc.getY() == 0) {
			if(o == Orientation.NORTH) {
				return false;
			}
			if(o == Orientation.WEST || o == Orientation.EAST) {
				if(d == Direction.NORTH) {
					return false;
				}
			}
		}
		if (cc.getY() == getHeight() - 1) {
			if(o == Orientation.SOUTH) {
				return false;
			}
			if(o == Orientation.WEST || o == Orientation.EAST) {
				if(d == Direction.SOUTH) {
					return false;
				}
			}
		}
		if(o == Orientation.NORTH) {
			if(d == Direction.SOUTH) {
				return false;
			}
		}
		if(o == Orientation.SOUTH) {
			if(d == Direction.NORTH) {
				return false;
			}
		}
		if(o == Orientation.WEST) {
			if(d == Direction.EAST) {
				return false;
			}
		}
		if(o == Orientation.EAST) {
			if(d == Direction.WEST) {
				return false;
			}
		}
		
		Coordinates respStart = getResponseStart(cc, o);
		if(getCell(respStart).getValue() instanceof ClueResponseInstance) {
			return false;
		}
		
		return true;
	}
	

	private Coordinates getResponseStart(Coordinates cc, Orientation o) {
		if (cc == null || o == null)
			throw new IllegalArgumentException();
		if(o == Orientation.NORTH) {
			return new Coordinates(cc.getX(), cc.getY() - 1);
		}
		if(o == Orientation.SOUTH) {
			return new Coordinates(cc.getX(), cc.getY() + 1);
		}
		if(o == Orientation.WEST) {
			return new Coordinates(cc.getX() - 1, cc.getY());
		}
		if(o == Orientation.EAST) {
			return new Coordinates(cc.getX() + 1, cc.getY());
		}
		throw new IllegalArgumentException();
	}

	private Coordinates getMaxResponseEnd(Coordinates cc, Direction d) {
		if (cc == null || d == null)
			throw new IllegalArgumentException();
		if (d == Direction.NORTH) {
			Coordinates maxCc = new Coordinates(cc.getX(), cc.getY());
			while (maxCc.getY() > 0) {
				maxCc = new Coordinates(maxCc.getX(), maxCc.getY() - 1);
				Value value = getCell(maxCc).getValue();
				if (value instanceof ClueResponseInstance) {
					maxCc = new Coordinates(maxCc.getX(), maxCc.getY() + 1);
					return maxCc;
				}
			}
			return maxCc;
		}
		else if (d == Direction.SOUTH) {
			Coordinates maxCc = new Coordinates(cc.getX(), cc.getY());
			while (maxCc.getY() < getHeight() - 1) {
				maxCc = new Coordinates(maxCc.getX(), maxCc.getY() + 1);
				Value value = getCell(maxCc).getValue();
				if (value instanceof ClueResponseInstance) {
					maxCc = new Coordinates(maxCc.getX(), maxCc.getY() - 1);
					return maxCc;
				}
			}
			return maxCc;
		}
		else if (d == Direction.WEST) {
			Coordinates maxCc = new Coordinates(cc.getX(), cc.getY());
			while (maxCc.getX() > 0) {
				maxCc = new Coordinates(maxCc.getX() - 1, maxCc.getY());
				Value value = getCell(maxCc).getValue();
				if (value instanceof ClueResponseInstance) {
					maxCc = new Coordinates(maxCc.getX() + 1, maxCc.getY());
					return maxCc;
				}
			}
			return maxCc;
		}
		else if (d == Direction.EAST) {
			Coordinates maxCc = new Coordinates(cc.getX(), cc.getY());
			while (maxCc.getX() < getWidth() - 1) {
				maxCc = new Coordinates(maxCc.getX() + 1, maxCc.getY());
				Value value = getCell(maxCc).getValue();
				if (value instanceof ClueResponseInstance) {
					maxCc = new Coordinates(maxCc.getX() - 1, maxCc.getY());
					return maxCc;
				}
			}
			return maxCc;
		}
		throw new IllegalArgumentException();
	}
	
	private void insertClueResponseInstance(ClueResponseInstance cri) {
		getCell(cri.getCellCoordinates()).setValue(cri);
		getCell(cri.getCellCoordinates()).getOwners().add(cri);
		availableRandomCoordinates.remove(cri.getCellCoordinates());
		Coordinates respStart = getResponseStart(cri.getCellCoordinates(), cri.getOrientation());
		String response = cri.getClueResponse().getResponse();
		
			if(cri.getDirection() == Direction.NORTH) {
				int i = respStart.getY();
				while(i > respStart.getY() - response.length()) {
					Coordinates cc = new Coordinates(respStart.getX(), i);
					getCell(cc).setValue(new Letter(response.charAt(respStart.getY() - i)));
					getCell(cc).getOwners().add(cri);
					if(i == respStart.getY()) {
						getCell(cc).getFirstResponseOwners().add(cri);
					}
					availableRandomCoordinates.remove(cc);
					i--;
				}
			}
			if(cri.getDirection() == Direction.SOUTH) {
				int i = respStart.getY();
				while(i < response.length() + respStart.getY()) {
					Coordinates cc = new Coordinates(respStart.getX(), i);
					getCell(cc).setValue(new Letter(response.charAt(i - respStart.getY())));
					getCell(cc).getOwners().add(cri);
					if(i == respStart.getY()) {
						getCell(cc).getFirstResponseOwners().add(cri);
					}
					availableRandomCoordinates.remove(cc);
					i++;
				}
			}
			if(cri.getDirection() == Direction.WEST) {
				int i = respStart.getX();
				while(i > respStart.getX() - response.length()) {
					Coordinates cc = new Coordinates(i, respStart.getY());
					getCell(cc).setValue(new Letter(response.charAt(respStart.getX() - i)));
					getCell(cc).getOwners().add(cri);
					if(i == respStart.getX()) {
						getCell(cc).getFirstResponseOwners().add(cri);
					}
					availableRandomCoordinates.remove(cc);
					i--;
				}
			}
			if(cri.getDirection() == Direction.EAST) {
				int i = respStart.getX();
				while(i < response.length() + respStart.getX()) {
					Coordinates cc = new Coordinates(i, respStart.getY());
					getCell(cc).setValue(new Letter(response.charAt(i - respStart.getX())));
					getCell(cc).getOwners().add(cri);
					if(i == respStart.getX()) {
						getCell(cc).getFirstResponseOwners().add(cri);
					}
					availableRandomCoordinates.remove(cc);
					i++;
				}
			}
			fireBoardChangedEvent();
	}
	
	private void removeClueResponseInstance(ClueResponseInstance cri) {
		getCell(cri.getCellCoordinates()).setValue(null);
		getCell(cri.getCellCoordinates()).getOwners().remove(cri);
		availableRandomCoordinates.add(cri.getCellCoordinates());
		Coordinates respStart = getResponseStart(cri.getCellCoordinates(), cri.getOrientation());
		String response = cri.getClueResponse().getResponse();
		
			if(cri.getDirection() == Direction.NORTH) {
				int i = respStart.getY();
				while(i > respStart.getY() - response.length()) {
					Coordinates cc = new Coordinates(respStart.getX(), i);
					getCell(cc).getOwners().remove(cri);
					if(getCell(cc).getOwners().size() == 0) {
						getCell(cc).setValue(null);
						availableRandomCoordinates.add(cc);
					}
					if(i == respStart.getY()) {
						getCell(cc).getFirstResponseOwners().remove(cri);
					}
					
					i--;
				}
			}
			if(cri.getDirection() == Direction.SOUTH) {
				int i = respStart.getY();
				while(i < response.length() + respStart.getY()) {
					Coordinates cc = new Coordinates(respStart.getX(), i);
					getCell(cc).getOwners().remove(cri);
					if(getCell(cc).getOwners().size() == 0) {
						getCell(cc).setValue(null);
						availableRandomCoordinates.add(cc);
					}
					if(i == respStart.getY()) {
						getCell(cc).getFirstResponseOwners().remove(cri);
					}
					
					i++;
				}
			}
			if(cri.getDirection() == Direction.WEST) {
				int i = respStart.getX();
				while(i > respStart.getX() - response.length()) {
					Coordinates cc = new Coordinates(i, respStart.getY());
					getCell(cc).getOwners().remove(cri);
					if(getCell(cc).getOwners().size() == 0) {
						getCell(cc).setValue(null);
						availableRandomCoordinates.add(cc);
					}
					if(i == respStart.getX()) {
						getCell(cc).getFirstResponseOwners().remove(cri);
					}
					
					i--;
				}
			}
			if(cri.getDirection() == Direction.EAST) {
				int i = respStart.getX();
				while(i < response.length() + respStart.getX()) {
					Coordinates cc = new Coordinates(i, respStart.getY());
					getCell(cc).getOwners().remove(cri);
					if(getCell(cc).getOwners().size() == 0) {
						getCell(cc).setValue(null);
						availableRandomCoordinates.add(cc);
					}
					if(i == respStart.getX()) {
						getCell(cc).getFirstResponseOwners().remove(cri);
					}
					
					i++;
				}
			}
			fireBoardChangedEvent();
	}

	private String getHint(Coordinates coords, Orientation o, Direction d) {
		StringBuilder hintBuilder = new StringBuilder();
		Coordinates respStart = getResponseStart(coords, o);
		Coordinates maxRespEnd = getMaxResponseEnd(respStart, d);
				
		if (respStart.getX() == maxRespEnd.getX()) {
			if(d == Direction.NORTH) {
				int i = respStart.getY();
				while(i >= maxRespEnd.getY()) {
					Coordinates cc = new Coordinates(respStart.getX(), i);
					if(getCell(cc).getValue() instanceof Letter) {
						hintBuilder.append(((Letter)getCell(cc).getValue()).getCharacter());
					}
					else {
						hintBuilder.append('_');
					}
					i--;
				}
			}
			if(d == Direction.SOUTH) {
				int i = respStart.getY();
				while(i <= maxRespEnd.getY()) {
					Coordinates cc = new Coordinates(respStart.getX(), i);
					if(getCell(cc).getValue() instanceof Letter) {
						hintBuilder.append(((Letter)getCell(cc).getValue()).getCharacter());
					}
					else {
						hintBuilder.append('_');
					}
					i++;
				}
			}
		}
		if (respStart.getY() == maxRespEnd.getY()) {
			if(d == Direction.WEST) {
				int i = respStart.getX();
				while(i >= maxRespEnd.getX()) {
					Coordinates cc = new Coordinates(i, respStart.getY());
					if(getCell(cc).getValue() instanceof Letter) {
						hintBuilder.append(((Letter)getCell(cc).getValue()).getCharacter());
					}
					else {
						hintBuilder.append('_');
					}
					i--;
				}
			}
			if(d == Direction.EAST) {
				int i = respStart.getX();
				while(i <= maxRespEnd.getX()) {
					Coordinates cc = new Coordinates(i, respStart.getY());
					if(getCell(cc).getValue() instanceof Letter) {
						hintBuilder.append(((Letter)getCell(cc).getValue()).getCharacter());
					}
					else {
						hintBuilder.append('_');
					}
					i++;
				}
			}
		}
		return hintBuilder.toString();
	}
	
	public Boolean isBoardFull() {
		for(int y = 0; y < getHeight(); y++) {
			for(int x = 0; x < getWidth(); x++) {
				if(getCell(new Coordinates(x, y)).getValue() == null) {
					return false;
				}
			}
		}
		return true;
	}
	
	public void prettyPrint() {
		NumberFormat nf = new DecimalFormat();
		nf.setMinimumIntegerDigits(2);
		System.out.print("  ");
		for(int x = 0; x < getWidth(); x++) {
			System.out.print(x);
		}
		System.out.println();
		for(int y = 0; y < getHeight(); y++) {
			System.out.print(nf.format(y));
			for(int x = 0; x < getWidth(); x++) {
				Value v = getCell(new Coordinates(x, y)).getValue();
				if(v == null) {
					System.out.print("_");
				}
				else if (v instanceof ClueResponseInstance) {
					ClueResponseInstance cri = (ClueResponseInstance)v;
					if(cri.getOrientation() == Orientation.NORTH) {
						System.out.print("^");
					}
					if(cri.getOrientation() == Orientation.SOUTH) {
						System.out.print("/");
					}
					if(cri.getOrientation() == Orientation.WEST) {
						System.out.print("<");
					}
					if(cri.getOrientation() == Orientation.EAST) {
						System.out.print(">");
					}
				}
				else {
					System.out.print(v);
				}
			}
			System.out.println();
		}
	}
	
	// Randomization methods
	
	private Coordinates getRandomCoordinates() {
		int nextRandomCoordinates = 0;
		if(availableRandomCoordinates.size() > 1) {
			nextRandomCoordinates = random.nextInt(availableRandomCoordinates.size());
		}
		return availableRandomCoordinates.get(nextRandomCoordinates);
	}
	
	private Orientation getRandomOrientation(Coordinates cc) {
		List<Orientation> availableRandomOrientations = this.availableRandomOrientations.get(cc.toString());
		int nextRandomOrientation = 0;
		if(availableRandomOrientations.size() > 1) {
			nextRandomOrientation = random.nextInt(availableRandomOrientations.size());
		}
		return availableRandomOrientations.get(nextRandomOrientation);
	}
	
	private Direction getRamdomDirection(Coordinates cc, Orientation o) {
		List<Direction> availableRandomDirections = this.availableRandomDirections.get(cc.toString() + o.toString());
		int nextRandomDirection = 0;
		if(availableRandomDirections.size() > 1) {
			nextRandomDirection = random.nextInt(availableRandomDirections.size());
		}
		return availableRandomDirections.get(nextRandomDirection);
	}
	
	private ClueResponse getRamdomClueResponse(Coordinates cc, Orientation o, Direction d) {
		List<ClueResponse> availableRandomClueResponses = this.availableRandomClueResponses.get(cc.toString() + o.toString() + d.toString());
		int nextRandomClueResponse = 0;
		if(availableRandomClueResponses.size() > 1) {
			nextRandomClueResponse = random.nextInt(availableRandomClueResponses.size());
		}
		return availableRandomClueResponses.get(nextRandomClueResponse);
	}
	
	private int getAvailableRandomCoordinatesSize() {
		return availableRandomCoordinates.size();
	}

	private int getAvailableRandomOrientationsSize(Coordinates cc) {
		return availableRandomOrientations.get(cc.toString()).size();
	}
	
	private int getAvailableRamdomDirectionsSize(Coordinates cc, Orientation o) {
		return availableRandomDirections.get(cc.toString() + o.toString()).size();
	}
	
	private int getAvailableRamdomClueResponsesSize(Coordinates cc, Orientation o, Direction d) {
		return availableRandomClueResponses.get(cc.toString() + o.toString() + d.toString()).size();
	}
	
	private void fireBoardChangedEvent() {
		for (BoardChangeListener listener : boardChangeListeners) {
			listener.boardChanged();
		}
	}
	
//	private int getMaxResponseLength(Coordinates respStart, Coordinates respEnd) {
//	if(respStart == null || respEnd == null) {
//		throw new IllegalArgumentException();
//	}
//	if (respStart.getX() == respEnd.getX()) {
//			return Math.abs(respStart.getY() - respEnd.getY());
//	}
//	if (respStart.getY() == respEnd.getY()) {
//			return Math.abs(respStart.getX() - respEnd.getX());
//	}
//	throw new IllegalArgumentException();
//}

}